/*
 * Copyright (c) 2010-2011 Josef Hardi <josef.hardi@gmail.com>
 * Copyright (c) 2010 Piotr Zagawa <piotr.zagawa@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package id.duapuluhdua.dakidaki.compass;

import java.util.Calendar;

import android.content.Context;
import android.hardware.GeomagneticField;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.location.Location;
import android.widget.Toast;

import id.duapuluhdua.dakidaki.R;
import id.duapuluhdua.dakidaki.UserAlert;

/**
 * Define an easy reader of the magnetic sensor.
 */
public class Compass
{
  /** Fields */
  private Context mContext;
  private SensorManager mSensorManager;
  private Sensor mSensor;
  private GeomagneticField mGeoField;
  private Calendar mCalendar;

  private float mTrueNorthDeclination;
  private float mAzimuth;
  private float mPitch;
  private float mRoll;

  private String mMsgCalibrate;

  private final Boolean bDataMutex;

  private boolean bCalibrateLow;
  private boolean bCalibrateMedium;

  /**
   * The constructor.
   *
   * @param context the application environment.
   */
  public Compass(Context context)
  {
    mContext = context;
    mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
    mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION);

    mAzimuth = 0;
    mPitch = 0;
    mRoll = 0;
    mTrueNorthDeclination = 0;

    bCalibrateLow = false;
    bCalibrateMedium = false;
    bDataMutex = false;

    mCalendar = Calendar.getInstance();

    mMsgCalibrate = context.getString(R.string.alert_calibrate_compass);
  }

  /**
   * Check and update the current amount of declination (in angular degree)
   * on a specific location. This measure will be added in the azimuth
   * calculation.
   *
   * @param location the current location.
   */
  public void updateTrueNorthCorrection(Location location)
  {
    if (mGeoField == null) {
      final float latitude = (float)location.getLatitude();
      final float longitude = (float)location.getLongitude();
      final float altitude = (float)location.getAltitude();

      final long time = mCalendar.getTimeInMillis();
      mGeoField = new GeomagneticField(latitude, longitude, altitude, time);

      mTrueNorthDeclination = mGeoField.getDeclination();
    }
  }

  /**
   * Register a default listener for the sensor.
   */
  public void start()
  {
    if (mSensor != null) {
      final int rate = SensorManager.SENSOR_DELAY_NORMAL;
      mSensorManager.registerListener(mSensorEvent, mSensor, rate);
    }
  }

  /**
   * Unregister the sensor listener.
   */
  public void stop()
  {
    if (mSensor != null) {
      mSensorManager.unregisterListener(mSensorEvent);
    }
  }

  /**
   * Get the direction of an observer to a point of interest, expressed as the
   * angular distance from the reference point on a plane. Since in mapping the
   * reference plane is the horizon then the reference point is the north
   * point. Thus, the azimuth is defined as the angular degree from the
   * north measured clockwise plus the declination degree.
   *
   * @return the total azimuth value (in degree).
   */
  public float getAzimuth()
  {
    float value = 0;
    synchronized (bDataMutex)
    {
      // Get azimuth and correct with declination
      value = mAzimuth + mTrueNorthDeclination;
      if (value > 359) {
        value -= 360;
      }
    }
    return value;
  }

  /**
   * Get the angular distance when the device is being rotated outward or
   * inward. Positive value indicates the device is being rotated outward,
   * and vice versa. Value range: [180, -180].
   *
   * @return the angular distance of the rotation.
   */
  public float getPitch()
  {
    float value = 0;
    synchronized (bDataMutex)
    {
      value = mPitch;
    }
    return value;
  }

  /**
   * Get the angular distance when the device is being rotated left or right.
   * Positive value indicate the device is being rotated left, and vice versa.
   * Value range: [90, -90].
   *
   * @return the angular distance of the rotation.
   */
  public float getRoll()
  {
    float value = 0;
    synchronized (bDataMutex)
    {
      value = mRoll;
    }
    return value;
  }

  // The sensor event listener
  private final SensorEventListener mSensorEvent = new SensorEventListener()
  {
    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy)
    {
      if (sensor.getType() == Sensor.TYPE_ORIENTATION) {
        if (accuracy == SensorManager.SENSOR_STATUS_ACCURACY_LOW) {
          if (!bCalibrateLow) {
            bCalibrateLow = true;
            UserAlert.show(mContext, mMsgCalibrate);
          }
        }
        if (accuracy == SensorManager.SENSOR_STATUS_ACCURACY_MEDIUM) {
          if (!bCalibrateMedium) {
            bCalibrateMedium = true;
            Toast.makeText(mContext, mMsgCalibrate, Toast.LENGTH_LONG).show();
          }
        }
      }
    }

    @Override
    public void onSensorChanged(SensorEvent event)
    {
      if (event.sensor.getType() == Sensor.TYPE_ORIENTATION) {
        synchronized (bDataMutex)
        {
          mAzimuth = event.values[0];
          mPitch = event.values[1];
          mRoll = event.values[2];
        }
      }
    }
  };
}